package pkg

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"github.com/SkyAPM/go2sky"
	"io"
	"mime/multipart"
	"net/http"
	"net/url"
	agentV3 "skywalking.apache.org/repo/goapi/collect/language/agent/v3"
	"strconv"
	"strings"
	"time"
)

const (
	defaultTimeout = 10
)

type HTTPPkg struct {
	withTx      bool
	header      map[string]string
	timeout     int
	contentType string
	statusCode  int
	resCode     int
	resMsg      string
	result      string
	err         error
}

var HTTP = &HTTPPkg{}

func NewHTTP() *HTTPPkg {
	return &HTTPPkg{
		header:      map[string]string{},
		timeout:     10,
		contentType: "urlencoded",
	}
}

func (p *HTTPPkg) GetResCode() int {
	return p.resCode
}

func (p *HTTPPkg) GetResMsg() string {
	return p.resMsg
}

func (p *HTTPPkg) GetResBody() string {
	return p.result
}

func (p *HTTPPkg) GetStatusCode() int {
	return p.statusCode
}

func (p *HTTPPkg) Set(param string, value string) *HTTPPkg {
	p.header[param] = value
	return p
}

func (p *HTTPPkg) WithHeader(header map[string]string) *HTTPPkg {
	for key, value := range header {
		p.header[key] = value
	}
	return p
}

func (p *HTTPPkg) WithTrans() *HTTPPkg {
	p.withTx = true
	return p
}

func (p *HTTPPkg) WithTimeout(timeout int) *HTTPPkg {
	p.timeout = timeout
	return p
}

func (p *HTTPPkg) WithContentType(contentType string) *HTTPPkg {
	p.contentType = contentType
	return p
}

func (p *HTTPPkg) Res(res any) *HTTPPkg {
	if p.err != nil {
		return p
	}

	var temp struct {
		Code int             `json:"code"`
		Data json.RawMessage `json:"data"`
		Msg  string          `json:"msg"`
	}

	err := json.Unmarshal([]byte(p.result), &temp)
	if err != nil {
		p.err = fmt.Errorf("failed to parse JSON: %v", err)
		return p
	}

	p.resCode = temp.Code
	p.resMsg = temp.Msg

	err = json.Unmarshal(temp.Data, res)
	if err == nil {
		return p
	}

	if strPtr, ok := res.(*string); ok {
		*strPtr = string(temp.Data)
	} else {
		p.err = fmt.Errorf("failed to parse JSON Data and target is not *string: %v", err)
	}

	return p
}

func (p *HTTPPkg) Request(c context.Context, link string, params map[string]interface{}, method string) *HTTPPkg {
	if p.timeout <= 0 {
		p.timeout = defaultTimeout
	}

	if method == http.MethodGet {
		queryParams := url.Values{}
		for key, value := range params {
			queryParams.Add(key, fmt.Sprintf("%v", value))
		}
		link += "?" + queryParams.Encode()
		p.result, p.err = p.doRequest(c, http.MethodGet, link, nil, p.timeout, p.header)
	} else if method == http.MethodPost {
		var body io.Reader

		switch p.contentType {
		case "urlencoded":
			formData := url.Values{}
			for field, value := range params {
				formData.Set(field, fmt.Sprintf("%v", value))
			}
			body = strings.NewReader(formData.Encode())
			p.header["Content-Type"] = "application/x-www-form-urlencoded"

		case "form-data":
			var b bytes.Buffer
			writer := multipart.NewWriter(&b)
			for field, value := range params {
				if file, ok := value.(io.Reader); ok {
					part, createFormErr := writer.CreateFormFile(field, field)
					if createFormErr != nil {
						p.err = createFormErr
						return p
					}
					_, createFormErr = io.Copy(part, file)
					if createFormErr != nil {
						p.err = createFormErr
						return p
					}
				} else {
					_ = writer.WriteField(field, fmt.Sprintf("%v", value))
				}
			}
			err := writer.Close()
			if err != nil {
				p.err = err
				return p
			}
			body = &b
			p.header["Content-Type"] = writer.FormDataContentType()

		case "raw":
			body = strings.NewReader(fmt.Sprintf("%v", params["raw"]))
			if _, ok := p.header["Content-Type"]; !ok {
				p.header["Content-Type"] = "application/json"
			}

		default:
			p.err = fmt.Errorf("unsupported Content-Type: %s", p.contentType)
			return p
		}

		p.result, p.err = p.doRequest(c, http.MethodPost, link, body, p.timeout, p.header)
	}
	return p
}

func (p *HTTPPkg) doRequest(c context.Context, method, link string, body io.Reader, timeout int, header map[string]string) (string, error) {
	client := http.Client{Timeout: time.Second * time.Duration(timeout)}
	req, err := http.NewRequest(method, link, body)
	if err != nil {
		return "", fmt.Errorf("failed to create request of http: %w", err)
	}

	span, err := Tracer.CreateExitSpan(c, ExitSpanOptions{
		OperationName: req.URL.Scheme + "://" + req.URL.Host + req.URL.Path,
		Component:     ComponentIDGINHttpServer,
		Peer:          req.URL.Host,
		Layer:         agentV3.SpanLayer_Http,
		Injector: func(key, value string) error {
			req.Header.Set(key, value)
			return nil
		},
	})
	if err == nil {
		defer span.End()
	}
	Tracer.SpanTag(span, go2sky.TagHTTPMethod, req.Method)
	Tracer.SpanTag(span, go2sky.TagURL, link)

	for key, value := range header {
		req.Header.Add(key, value)
	}

	resp, err := client.Do(req)
	if err != nil {
		Tracer.SpanErr(span, err.Error())
		return "", fmt.Errorf("failed to do http request: %w", err)
	}
	Tracer.SpanTag(span, go2sky.TagStatusCode, strconv.Itoa(resp.StatusCode))

	bodyBytes, _ := io.ReadAll(resp.Body)
	_ = resp.Body.Close()

	p.statusCode = resp.StatusCode
	return string(bodyBytes), nil
}

func (p *HTTPPkg) Get(c context.Context, link string, timeout int, header map[string]string) (string, error) {
	return p.doRequest(c, http.MethodGet, link, nil, timeout, header)
}

func (p *HTTPPkg) Post(c context.Context, link string, body io.Reader, timeout int, header map[string]string) (string, error) {
	return p.doRequest(c, http.MethodPost, link, body, timeout, header)
}
